﻿using System.Data;
using System.Runtime.Remoting.Messaging;
using System.Web;
using FluentNHibernate.Cfg;
using NHibernate.Cache;
using NHibernate.Cfg;

namespace NHibernate.Sidekick.Data.NHibernate
{
    /// <summary>
    /// Manages a single NHibernate <see cref="ISession"/> across a single persistence context (or transaction context).
    /// It utilizes a thread safe <see cref="CallContext"/> object to determine if the session is invoked
    /// from an <see cref="CallContext"/> or an <see cref="HttpContext"/>.
    /// </summary>
    public class SessionManager
    {
        /// <summary>
        /// Specifies the NHibernate configuration.
        /// </summary>
        public enum NHibernationConfiguration
        {
            /// <summary>
            /// Standard XML (.hbm.xml) mapping configuration.
            /// </summary>
            Traditional, 

            /// <summary>
            /// Strongly typed C# code mapping configuration.
            /// </summary>
            Fluent
        }

        #region Thread-safe, lazy Singleton

        private volatile bool _isInstantiated;

        /// <summary>
        /// This is a thread-safe, lazy singleton.
        /// </summary>
        public static SessionManager Instance
        {
            get
            {
                return Nested.SessionSessionManager;
            }
        }

        /// <summary>
        /// Initializes the <see cref="SessionManager"/> factory upon instantiation.
        /// </summary>
        private SessionManager()
        {
        }

        /// <summary>
        /// Assists with ensuring thread-safe, lazy singleton.
        /// </summary>
        private class Nested
        {
            internal static readonly SessionManager SessionSessionManager = new SessionManager();
        }
        #endregion


        

        private volatile bool IsFluentlyConfigurated;
        private volatile FluentConfiguration FluentConfiguration;
        /// <summary>
        /// Sets the <see cref="SessionFactory"/> and returns the <see cref="ISession"/>.
        /// </summary>
        /// <param name="fluentConfiguration">Strongly typed C# code mapping configuration.</param>
        /// <param name="interceptor">The <see cref="IInterceptor"/>.</param>
        /// <returns>The main runtime interface between a .NET application and NHibernate.</returns>
        public ISession SetSessionFactoryAndReturnSession(FluentConfiguration fluentConfiguration, IInterceptor interceptor)
        {
            IsFluentlyConfigurated = true;
            FluentConfiguration = fluentConfiguration;
            NHibConfiguration = NHibernationConfiguration.Fluent;
            return GetSession(interceptor, true);
        }

        private volatile Configuration TraditionalConfiguration;
        /// <summary>
        /// Sets the <see cref="SessionFactory"/> and returns the <see cref="ISession"/>.
        /// </summary>
        /// <param name="traditionalConfiguration">Standard XML (.hbm.xml) mapping configuration.</param>
        /// <param name="interceptor">The <see cref="IInterceptor"/>.</param>
        /// <returns>The main runtime interface between a .NET application and NHibernate.</returns>
        public ISession SetSessionFactoryAndReturnSession(Configuration traditionalConfiguration, IInterceptor interceptor)
        {
            TraditionalConfiguration = traditionalConfiguration;
            NHibConfiguration = NHibernationConfiguration.Traditional;
            return GetSession(interceptor, true);
        }

        /// <summary>
        /// Gets an <see cref="ISession"/> with or without an <see cref="IInterceptor"/>.  
        /// </summary>
        /// <remarks>
        /// This method is not called directly;
        /// Instead, it gets invoked from other public methods.
        /// </remarks>
        [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Assert, Unrestricted = true)]
        public ISession GetSession()
        {
            return IsFluentlyConfigurated
                       ? GetSession(FluentConfiguration)
                       : GetSession(TraditionalConfiguration);
        }

        /// <summary>
        /// Gets the current <see cref="ISession"/>.
        /// </summary>
        /// <param name="traditionalConfiguration">Standard XML (.hbm.xml) mapping configuration.</param>
        /// <returns>The main runtime interface between a .NET application and NHibernate.</returns>
        public ISession GetSession(Configuration traditionalConfiguration)
        {
            return GetSession(null, false);
        }

        /// <summary>
        /// Gets the current <see cref="ISession"/>.
        /// </summary>
        /// <param name="fluentConfiguration">Strongly typed C# code mapping configuration.</param>
        /// <returns>The main runtime interface between a .NET application and NHibernate.</returns>
        public ISession GetSession(FluentConfiguration fluentConfiguration)
        {
            return GetSession(null, false);
        }

        private volatile NHibernationConfiguration NHibConfiguration;
        /// <summary>
        /// Gets an <see cref="ISession"/> with or without an <see cref="IInterceptor"/>.  
        /// </summary>
        /// <remarks>
        /// This method is not called directly;
        /// Instead, it gets invoked from other public methods.
        /// </remarks>
        [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Assert, Unrestricted = true)]
        public ISession GetSession(IInterceptor interceptor, bool hasInterceptor)
        {
            if (!_isInstantiated) InitSessionFactory();

            var session = ContextSession;

            if (session == null)
            {
                session = interceptor != null
                              ? Instance.SessionFactory.OpenSession(interceptor)
                              : Instance.SessionFactory.OpenSession();

                ContextSession = session;
            }

            //Check.Ensure(session != null, "session was null");

            return session;
        }

        private const string TransactionKey = "CONTEXT_TRANSACTION";
        private const string SessionKey = "CONTEXT_SESSION";
        private ISessionFactory SessionFactory { get; set; }

        /// <summary>
        /// Instantiate a new <see cref="ISessionFactory"/>, using
        ///  the properties and mappings in this configuration.
        /// </summary>
        /// <remarks>
        /// The <see cref="ISessionFactory"/> will be immutable, so changes made to the
        /// configuration after building the <see cref="ISessionFactory"/> will not affect it.
        /// </remarks>
        private void InitSessionFactory()
        {
            switch (NHibConfiguration)
            {
                case NHibernationConfiguration.Traditional:
                    SessionFactory = TraditionalConfiguration.BuildSessionFactory();
                    break;

                case NHibernationConfiguration.Fluent:
                    SessionFactory = FluentConfiguration.BuildSessionFactory();
                    break;
            }

            _isInstantiated = true;
        }

        /// <summary>
        /// Allows you to register an interceptor on a new session.  This may not be called if there is already
        /// an open session attached to the <see cref="HttpContext"/>.  
        /// </summary>
        /// <remarks>
        /// If you have an interceptor to be used, modify the <see cref="IHttpModule"/>
        /// to call this before calling <see cref="BeginTransaction()"/>.
        /// </remarks>
        private void RegisterInterceptor(IInterceptor interceptor)
        {
            var session = ContextSession;

            if (session != null && session.IsOpen)
            {
                throw new CacheException("You cannot register an interceptor once a session has already been opened");
            }

            GetSession(interceptor, true);
        }

        /// <summary>
        /// Flushes anything left in the <see cref="ISession"/> and closes the <see cref="IDbConnection"/>.
        /// </summary>
        private static void CloseSession()
        {
            var session = ContextSession;

            if (session != null && session.IsOpen)
            {
                session.Flush();
                session.Close();
            }

            ContextSession = null;
        }

        /// <summary>
        /// Begins a unit of work and returns the associated <see cref="ITransaction"/>.
        /// </summary>
        private void BeginTransaction()
        {
            var transaction = ContextTransaction;

            if (transaction == null)
            {
                transaction = GetSession().BeginTransaction();
                ContextTransaction = transaction;
            }
        }

        /// <summary>
        /// Flush the associated <see cref="ISession"/> and the unit of work.
        /// </summary>
        internal void CommitTransaction()
        {
            var transaction = ContextTransaction;

            try
            {
                if (HasOpenTransaction())
                {
                    transaction.Commit();
                    ContextTransaction = null;
                }
            }
            catch (HibernateException)
            {
                RollbackTransaction();
                throw;
            }
        }

        /// <summary>
        /// Determines if there's an open <see cref="ITransaction"/>.
        /// </summary>
        internal bool HasOpenTransaction()
        {
            var transaction = ContextTransaction;

            return transaction != null
                   && !transaction.WasCommitted
                   && !transaction.WasRolledBack;
        }

        /// <summary>
        /// Forces the underlying <see cref="ITransaction"/> to roll back.
        /// </summary>
        internal void RollbackTransaction()
        {
            var transaction = ContextTransaction;

            try
            {
                if (HasOpenTransaction())
                {
                    transaction.Rollback();
                    var session = ContextSession;
                    session.Clear();
                }

                ContextTransaction = null;
            }
            finally
            {
                CloseSession();
            }
        }

        /// <summary>
        /// If within a web context, this uses <see cref="HttpContext"/>
        /// instead of the WinForms specific <see cref="CallContext"/>.
        /// </summary>
        private static ITransaction ContextTransaction
        {
            get
            {
                if (IsInWebContext())
                {
                    return (ITransaction)HttpContext.Current.Items[TransactionKey];
                }
                else
                {
                    return (ITransaction)CallContext.GetData(TransactionKey);
                }
            }
            set
            {
                if (IsInWebContext())
                {
                    HttpContext.Current.Items[TransactionKey] = value;
                }
                else
                {
                    CallContext.SetData(TransactionKey, value);
                }
            }
        }

        /// <summary>
        /// If within a web context, this uses <see cref="HttpContext"/>
        /// instead of the WinForms specific <see cref="CallContext"/>.
        /// </summary>
        protected static ISession ContextSession
        {
            [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Assert, Unrestricted = true)]
            get
            {
                if (IsInWebContext())
                {
                    return (ISession)HttpContext.Current.Items[SessionKey];
                }
                else
                {
                    return (ISession)CallContext.GetData(SessionKey);
                }
            }
            set
            {
                if (IsInWebContext())
                {
                    HttpContext.Current.Items[SessionKey] = value;
                }
                else
                {
                    CallContext.SetData(SessionKey, value);
                }
            }
        }

        /// <summary>
        /// Determines whether the <see cref="ISession"/> is in a web context.
        /// </summary>
        private static bool IsInWebContext()
        {
            return HttpContext.Current != null;
        }
    }
}
